import { assertEquals, assertMatch } from '@std/assert';

import { Calendar, CalendarEvent } from '/lib/models/calendar.ts';
import {
  CALENDAR_COLOR_OPTIONS,
  convertRRuleToWords,
  generateVCalendar,
  generateVEvent,
  getCalendarEventStyle,
  getColorAsHex,
  getDateRangeForCalendarView,
  getDaysForWeek,
  getIdFromVEvent,
  getWeeksForMonth,
  parseIcsDate,
  parseVCalendar,
  splitTextIntoVEvents,
  updateIcs,
} from './calendar.ts';

Deno.test('that getColorAsHex works', () => {
  const tests: { input: string; expected: string | undefined }[] = [
    { input: 'bg-red-700', expected: '#B51E1F' },
    { input: 'bg-green-700', expected: '#148041' },
    { input: 'bg-blue-900', expected: '#1E3A89' },
    { input: 'bg-purple-800', expected: '#6923A9' },
    { input: 'bg-gray-700', expected: '#384354' },
    { input: 'invalid-color', expected: '#384354' },
  ];

  for (const test of tests) {
    const output = getColorAsHex(test.input as typeof CALENDAR_COLOR_OPTIONS[number]);
    assertEquals(output, test.expected);
  }
});

Deno.test('that getIdFromVEvent works', () => {
  const tests: { input: string; expected?: string; shouldBeUUID?: boolean }[] = [
    {
      input: `BEGIN:VEVENT
UID:12345-abcde-67890
SUMMARY:Test Event
END:VEVENT`,
      expected: '12345-abcde-67890',
    },
    {
      input: `BEGIN:VEVENT
SUMMARY:No UID Event
END:VEVENT`,
      shouldBeUUID: true,
    },
    {
      input: `BEGIN:VEVENT
UID:   spaced-uid   
SUMMARY:Spaced UID
END:VEVENT`,
      expected: 'spaced-uid',
    },
  ];

  for (const test of tests) {
    const output = getIdFromVEvent(test.input);
    if (test.expected) {
      assertEquals(output, test.expected);
    } else if (test.shouldBeUUID) {
      assertMatch(output, /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
    }
  }
});

Deno.test('that splitTextIntoVEvents works', () => {
  const tests: { input: string; expected: string[] }[] = [
    {
      input: `BEGIN:VEVENT
UID:1
SUMMARY:Event 1
END:VEVENT
BEGIN:VEVENT
UID:2
SUMMARY:Event 2
END:VEVENT`,
      expected: [
        `BEGIN:VEVENT
UID:1
SUMMARY:Event 1
END:VEVENT`,
        `BEGIN:VEVENT
UID:2
SUMMARY:Event 2
END:VEVENT`,
      ],
    },
    {
      input: `BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
BEGIN:VEVENT
UID:1
SUMMARY:Event 1
END:VEVENT
BEGIN:VEVENT
UID:2
SUMMARY:Event 2
END:VEVENT
END:VCALENDAR`,
      expected: [
        `BEGIN:VEVENT
UID:1
SUMMARY:Event 1
END:VEVENT`,
        `BEGIN:VEVENT
UID:2
SUMMARY:Event 2
END:VEVENT`,
      ],
    },
    {
      input: `BEGIN:VEVENT
UID:single
SUMMARY:Single Event
END:VEVENT`,
      expected: [
        `BEGIN:VEVENT
UID:single
SUMMARY:Single Event
END:VEVENT`,
      ],
    },
    {
      input: '',
      expected: [],
    },
    {
      input: `BEGIN:VEVENT
UID:incomplete
SUMMARY:Incomplete Event`,
      expected: [],
    },
  ];

  for (const test of tests) {
    const output = splitTextIntoVEvents(test.input);
    assertEquals(output, test.expected);
  }
});

Deno.test('that getDateRangeForCalendarView works', () => {
  const baseDate = '2025-01-15';

  const dayRange = getDateRangeForCalendarView(baseDate, 'day');
  assertEquals(dayRange.start.getDate(), 14); // Previous day
  assertEquals(dayRange.end.getDate(), 16); // Next day

  const weekRange = getDateRangeForCalendarView(baseDate, 'week');
  assertEquals(weekRange.start.getDate(), 8); // 7 days before
  assertEquals(weekRange.end.getDate(), 22); // 7 days after

  const monthRange = getDateRangeForCalendarView(baseDate, 'month');
  assertEquals(monthRange.start.getDate(), 8); // 7 days before
  assertEquals(monthRange.end.getDate(), 15); // 31 days after (wraps to next month)
});

Deno.test('that generateVEvent works', () => {
  const testEvents: {
    input: {
      calendarEvent: CalendarEvent;
      createdDate: Date;
    };
    expected: string;
  }[] = [
    {
      input: {
        calendarEvent: {
          calendarId: 'test-calendar',
          isAllDay: false,
          url: 'test-123.ics',
          uid: 'test-123',
          title: 'Test Event',
          startDate: new Date('2025-01-15T10:00:00Z'),
          endDate: new Date('2025-01-15T11:00:00Z'),
          organizerEmail: 'test@example.com',
          transparency: 'opaque',
          isRecurring: false,
          recurringRrule: undefined,
          sequence: 0,
          description: 'Test description',
          location: 'Test location',
          eventUrl: 'https://example.com',
          attendees: [
            { email: 'attendee@example.com', status: 'accepted', name: 'Test Attendee' },
          ],
          reminders: [
            { type: 'display', startDate: '2025-01-15T09:45:00Z', description: 'Test reminder' },
          ],
        },
        createdDate: new Date('2025-01-15T10:00:00Z'),
      },
      expected: `BEGIN:VEVENT
DTSTAMP:20250115T100000
DTSTART:20250115T100000
DTEND:20250115T110000
ORGANIZER;CN=:mailto:test@example.com
SUMMARY:Test Event
TRANSP:OPAQUE
UID:test-123
SEQUENCE:0
CREATED:20250115T100000
LAST-MODIFIED:20250115T100000
DESCRIPTION:Test description
LOCATION:Test location
URL:https://example.com
ATTENDEE;PARTSTAT=ACCEPTED;CN=Test Attendee:mailto:attendee@example.com
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:Test reminder
TRIGGER;VALUE=DATE-TIME:20250115T094500
END:VALARM
END:VEVENT`,
    },
    {
      input: {
        calendarEvent: {
          calendarId: 'test-calendar',
          isAllDay: true,
          url: 'test-123.ics',
          uid: 'test-123',
          title: 'Test Event',
          startDate: new Date('2025-01-15T10:00:00Z'),
          endDate: new Date('2025-01-15T11:00:00Z'),
          organizerEmail: 'test@example.com',
          transparency: 'opaque',
          isRecurring: false,
          recurringRrule: undefined,
          sequence: 0,
          description: 'Test description',
          location: 'Test location',
          eventUrl: 'https://example.com',
          attendees: [
            { email: 'attendee@example.com', status: 'accepted', name: 'Test Attendee' },
          ],
          reminders: [
            { type: 'display', startDate: '2025-01-15T09:45:00Z', description: 'Test reminder' },
          ],
        },
        createdDate: new Date('2025-01-15T10:00:00Z'),
      },
      expected: `BEGIN:VEVENT
DTSTAMP:20250115T100000
DTSTART;VALUE=DATE:20250115
DTEND;VALUE=DATE:20250115
ORGANIZER;CN=:mailto:test@example.com
SUMMARY:Test Event
TRANSP:OPAQUE
UID:test-123
SEQUENCE:0
CREATED:20250115T100000
LAST-MODIFIED:20250115T100000
DESCRIPTION:Test description
LOCATION:Test location
URL:https://example.com
ATTENDEE;PARTSTAT=ACCEPTED;CN=Test Attendee:mailto:attendee@example.com
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:Test reminder
TRIGGER;VALUE=DATE-TIME:20250115T094500
END:VALARM
END:VEVENT`,
    },
    {
      input: {
        calendarEvent: {
          calendarId: 'test-calendar',
          isAllDay: false,
          url: 'test-123.ics',
          uid: 'test-123',
          title: 'Test Event',
          startDate: new Date('2025-01-15T10:00:00Z'),
          endDate: new Date('2025-01-15T11:00:00Z'),
          organizerEmail: 'test@example.com',
          transparency: 'opaque',
          isRecurring: true,
          recurringRrule: 'FREQ=WEEKLY;BYDAY=MO',
          sequence: 1,
        },
        createdDate: new Date('2025-01-15T10:00:00Z'),
      },
      expected: `BEGIN:VEVENT
DTSTAMP:20250115T100000
DTSTART:20250115T100000
DTEND:20250115T110000
ORGANIZER;CN=:mailto:test@example.com
SUMMARY:Test Event
TRANSP:OPAQUE
UID:test-123
RRULE:FREQ=WEEKLY;BYDAY=MO
SEQUENCE:1
CREATED:20250115T100000
LAST-MODIFIED:20250115T100000
END:VEVENT`,
    },
  ];

  for (const testEvent of testEvents) {
    const output = generateVEvent(testEvent.input.calendarEvent, testEvent.input.createdDate);
    assertEquals(output, testEvent.expected);
  }
});

Deno.test('that generateVCalendar works', () => {
  const testEvents: CalendarEvent[] = [
    {
      calendarId: 'test-calendar',
      isAllDay: false,
      url: 'test-123.ics',
      uid: 'event-1',
      title: 'Event 1',
      startDate: new Date('2025-01-15T10:00:00Z'),
      endDate: new Date('2025-01-15T11:00:00Z'),
      organizerEmail: 'test@example.com',
      transparency: 'opaque',
      isRecurring: false,
      recurringRrule: undefined,
      sequence: 0,
    },
    {
      calendarId: 'test-calendar',
      isAllDay: true,
      url: 'test-123.ics',
      uid: 'event-2',
      title: 'Event 2',
      startDate: new Date('2025-01-16T10:00:00Z'),
      endDate: new Date('2025-01-16T11:00:00Z'),
      organizerEmail: 'test@example.com',
      transparency: 'opaque',
      isRecurring: false,
      recurringRrule: undefined,
      sequence: 0,
    },
  ];

  const output = generateVCalendar(testEvents, new Date('2025-01-15T10:00:00Z'));

  const expected = `BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
BEGIN:VEVENT
DTSTAMP:20250115T100000
DTSTART:20250115T100000
DTEND:20250115T110000
ORGANIZER;CN=:mailto:test@example.com
SUMMARY:Event 1
TRANSP:OPAQUE
UID:event-1
SEQUENCE:0
CREATED:20250115T100000
LAST-MODIFIED:20250115T100000
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20250115T100000
DTSTART;VALUE=DATE:20250116
DTEND;VALUE=DATE:20250116
ORGANIZER;CN=:mailto:test@example.com
SUMMARY:Event 2
TRANSP:OPAQUE
UID:event-2
SEQUENCE:0
CREATED:20250115T100000
LAST-MODIFIED:20250115T100000
END:VEVENT
END:VCALENDAR`;

  assertEquals(output, expected);
});

Deno.test('that updateIcs works', () => {
  const testEvents: {
    input: {
      originalIcs: string;
      updates: CalendarEvent;
    };
    expected: string;
  }[] = [
    {
      input: {
        originalIcs: `BEGIN:VEVENT
UID:test-123
SUMMARY:Original Title
DTSTART:20250115T100000Z
DTEND:20250115T110000Z
END:VEVENT`,
        updates: {
          calendarId: 'test-calendar',
          isAllDay: false,
          url: 'test-123.ics',
          uid: 'test-123',
          title: 'Updated Title',
          startDate: new Date('2025-01-16T10:00:00Z'),
          endDate: new Date('2025-01-16T11:00:00Z'),
          organizerEmail: 'test@example.com',
          transparency: 'transparent',
          isRecurring: false,
          recurringRrule: undefined,
          sequence: 0,
          description: 'New description',
          location: 'New location',
          eventUrl: 'https://updated.com',
        },
      },
      expected: `BEGIN:VEVENT
UID:test-123
SUMMARY:Updated Title
DTSTART:20250116T100000
DTEND:20250116T110000
DESCRIPTION:New description
URL:https://updated.com
LOCATION:New location
END:VEVENT`,
    },
    {
      input: {
        originalIcs: `BEGIN:VEVENT
UID:test-123
SUMMARY:Original Title
DTSTART:20250115T100000
DTEND:20250115T110000
URL:https://example.com
LOCATION:Example location
END:VEVENT`,
        updates: {
          calendarId: 'test-calendar',
          isAllDay: true,
          url: 'test-123.ics',
          uid: 'test-123',
          title: 'Updated Title',
          startDate: new Date('2025-01-16T10:00:00Z'),
          endDate: new Date('2025-01-16T11:00:00Z'),
          organizerEmail: 'test@example.com',
          transparency: 'transparent',
          isRecurring: false,
          recurringRrule: undefined,
          sequence: 0,
          description: 'New description',
          eventUrl: 'https://updated.com',
        },
      },
      expected: `BEGIN:VEVENT
UID:test-123
SUMMARY:Updated Title
DTSTART;VALUE=DATE:20250116
DTEND;VALUE=DATE:20250116
URL:https://updated.com
LOCATION:Example location
DESCRIPTION:New description
END:VEVENT`,
    },
    {
      input: {
        originalIcs: `BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//PYVOBJECT//NONSGML Version 0.9.9//EN
BEGIN:VEVENT
UID:test-123
SUMMARY:Original Title
DTSTART:20250115T100000
DTEND:20250115T110000
URL:https://example.com
LOCATION:Example location
END:VEVENT
END:VCALENDAR`,
        updates: {
          calendarId: 'test-calendar',
          isAllDay: true,
          url: 'test-123.ics',
          uid: 'test-123',
          title: 'Updated Title',
          startDate: new Date('2025-01-16T10:00:00Z'),
          endDate: new Date('2025-01-16T11:00:00Z'),
          organizerEmail: 'test@example.com',
          transparency: 'transparent',
          isRecurring: false,
          recurringRrule: undefined,
          sequence: 0,
          description: 'New description',
          eventUrl: 'https://updated.com',
        },
      },
      expected: `BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//PYVOBJECT//NONSGML Version 0.9.9//EN
BEGIN:VEVENT
UID:test-123
SUMMARY:Updated Title
DTSTART;VALUE=DATE:20250116
DTEND;VALUE=DATE:20250116
URL:https://updated.com
LOCATION:Example location
DESCRIPTION:New description
END:VEVENT
END:VCALENDAR`,
    },
    {
      input: {
        originalIcs: `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
BEGIN:VTIMEZONE
TZID:Europe/Lisbon
BEGIN:STANDARD
DTSTART:19111231T232315
RDATE:19111231T232315
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:-003645
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19161101T010000
RDATE:19161101T010000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19171015T000000
RDATE:19171015T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19181015T000000
RDATE:19181015T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19191015T000000
RDATE:19191015T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19201015T000000
RDATE:19201015T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19211015T000000
RDATE:19211015T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19241005T000000
RDATE:19241005T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19261003T000000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19291006T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19311004T000000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19341007T000000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19391119T000000
RDATE:19391119T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19401008T000000
RDATE:19401008T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19411006T000000
RDATE:19411006T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19421025T000000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19451028T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19461006T000000
RDATE:19461006T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19471005T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19651003T030000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19661002T030000
RDATE:19661002T030000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+010000
END:STANDARD
BEGIN:STANDARD
DTSTART:19760926T010000
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19770925T010000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19781001T020000
RDATE:19781001T020000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19790930T020000
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19800928T020000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19810927T010000
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19850929T010000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19860928T020000
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19910929T020000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19920927T020000
RDATE:19920927T020000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+010000
END:STANDARD
BEGIN:STANDARD
DTSTART:19930926T030000
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19950924T030000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+020000
TZOFFSETTO:+010000
END:STANDARD
BEGIN:STANDARD
DTSTART:19961027T020000
RDATE:19961027T020000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19971026T020000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
TZNAME:(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:19160617T230000
RDATE:19160617T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19170301T000000
RDATE:19170301T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19180301T000000
RDATE:19180301T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19190301T000000
RDATE:19190301T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19200301T000000
RDATE:19200301T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19210301T000000
RDATE:19210301T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19240416T230000
RDATE:19240416T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19260417T230000
RDATE:19260417T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19270409T230000
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=2SA;UNTIL=19280414T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19290420T230000
RDATE:19290420T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19310418T230000
RDATE:19310418T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19320402T230000
RDATE:19320402T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19340407T230000
RDATE:19340407T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19350330T230000
RDATE:19350330T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19360418T230000
RDATE:19360418T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19370403T230000
RDATE:19370403T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19380326T230000
RDATE:19380326T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19390415T230000
RDATE:19390415T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19400224T230000
RDATE:19400224T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19410405T230000
RDATE:19410405T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19420314T230000
RDATE:19420314T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19420425T230000
RDATE:19420425T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+010000
TZOFFSETTO:+020000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19420816T000000
RDATE:19420816T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+020000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19430313T230000
RDATE:19430313T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19430417T230000
RDATE:19430417T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+010000
TZOFFSETTO:+020000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19430829T000000
RDATE:19430829T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+020000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19440311T230000
RDATE:19440311T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19440422T230000
RDATE:19440422T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+010000
TZOFFSETTO:+020000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19440827T000000
RDATE:19440827T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+020000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19450310T230000
RDATE:19450310T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19450421T230000
RDATE:19450421T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+010000
TZOFFSETTO:+020000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19450826T000000
RDATE:19450826T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+020000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19460406T230000
RDATE:19460406T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19470406T020000
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU;UNTIL=19660403T020000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19770327T000000
RDATE:19770327T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19780402T010000
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU;UNTIL=19800406T010000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19810329T000000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19850331T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19860330T010000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19920329T010000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19930328T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19950326T020000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+010000
TZOFFSETTO:+020000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19960331T020000
RDATE:19960331T020000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+010000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19970330T010000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
TZNAME:(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
X-TZINFO:Europe/Lisbon[2025b]
END:VTIMEZONE
BEGIN:VEVENT
UID:99e15556-fd88-4cb9-818e-fcbf853bc443
DTSTART;TZID=Europe/Lisbon:20250720T090000
DTEND;TZID=Europe/Lisbon:20250720T100000
CREATED:20250720T075329Z
DTSTAMP:20250720T075414Z
RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;UNTIL=20250817T080000Z
SUMMARY:Recurring Standup
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR`,
        updates: {
          calendarId: 'test-calendar',
          isAllDay: false,
          url: '99e15556-fd88-4cb9-818e-fcbf853bc443.ics',
          uid: '99e15556-fd88-4cb9-818e-fcbf853bc443',
          title: 'Updated Title',
          startDate: new Date('2025-07-20T09:00:00Z'),
          endDate: new Date('2025-07-20T10:00:00Z'),
          organizerEmail: 'test@example.com',
          transparency: 'opaque',
          isRecurring: true,
          recurringRrule: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;UNTIL=20250817T080000Z',
          sequence: 0,
          description: 'New description',
        },
      },
      expected: `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
BEGIN:VTIMEZONE
TZID:Europe/Lisbon
BEGIN:STANDARD
DTSTART:19111231T232315
RDATE:19111231T232315
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:-003645
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19161101T010000
RDATE:19161101T010000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19171015T000000
RDATE:19171015T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19181015T000000
RDATE:19181015T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19191015T000000
RDATE:19191015T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19201015T000000
RDATE:19201015T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19211015T000000
RDATE:19211015T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19241005T000000
RDATE:19241005T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19261003T000000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19291006T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19311004T000000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19321002T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19341007T000000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19381002T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19391119T000000
RDATE:19391119T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19401008T000000
RDATE:19401008T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19411006T000000
RDATE:19411006T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19421025T000000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU;UNTIL=19451028T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19461006T000000
RDATE:19461006T000000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19471005T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19651003T030000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19661002T030000
RDATE:19661002T030000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+010000
END:STANDARD
BEGIN:STANDARD
DTSTART:19760926T010000
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19770925T010000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19781001T020000
RDATE:19781001T020000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19790930T020000
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19800928T020000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19810927T010000
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19850929T010000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19860928T020000
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19910929T020000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19920927T020000
RDATE:19920927T020000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+010000
END:STANDARD
BEGIN:STANDARD
DTSTART:19930926T030000
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19950924T030000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+020000
TZOFFSETTO:+010000
END:STANDARD
BEGIN:STANDARD
DTSTART:19961027T020000
RDATE:19961027T020000
TZNAME:Europe/Lisbon(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:STANDARD
DTSTART:19971026T020000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
TZNAME:(STD)
TZOFFSETFROM:+010000
TZOFFSETTO:+000000
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:19160617T230000
RDATE:19160617T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19170301T000000
RDATE:19170301T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19180301T000000
RDATE:19180301T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19190301T000000
RDATE:19190301T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19200301T000000
RDATE:19200301T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19210301T000000
RDATE:19210301T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19240416T230000
RDATE:19240416T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19260417T230000
RDATE:19260417T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19270409T230000
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=2SA;UNTIL=19280414T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19290420T230000
RDATE:19290420T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19310418T230000
RDATE:19310418T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19320402T230000
RDATE:19320402T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19340407T230000
RDATE:19340407T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19350330T230000
RDATE:19350330T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19360418T230000
RDATE:19360418T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19370403T230000
RDATE:19370403T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19380326T230000
RDATE:19380326T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19390415T230000
RDATE:19390415T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19400224T230000
RDATE:19400224T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19410405T230000
RDATE:19410405T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19420314T230000
RDATE:19420314T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19420425T230000
RDATE:19420425T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+010000
TZOFFSETTO:+020000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19420816T000000
RDATE:19420816T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+020000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19430313T230000
RDATE:19430313T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19430417T230000
RDATE:19430417T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+010000
TZOFFSETTO:+020000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19430829T000000
RDATE:19430829T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+020000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19440311T230000
RDATE:19440311T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19440422T230000
RDATE:19440422T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+010000
TZOFFSETTO:+020000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19440827T000000
RDATE:19440827T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+020000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19450310T230000
RDATE:19450310T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19450421T230000
RDATE:19450421T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+010000
TZOFFSETTO:+020000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19450826T000000
RDATE:19450826T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+020000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19460406T230000
RDATE:19460406T230000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19470406T020000
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU;UNTIL=19660403T020000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19770327T000000
RDATE:19770327T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19780402T010000
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU;UNTIL=19800406T010000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19810329T000000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19850331T000000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19860330T010000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19920329T010000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19930328T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU;UNTIL=19950326T020000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+010000
TZOFFSETTO:+020000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19960331T020000
RDATE:19960331T020000
TZNAME:Europe/Lisbon(DST)
TZOFFSETFROM:+010000
TZOFFSETTO:+010000
END:DAYLIGHT
BEGIN:DAYLIGHT
DTSTART:19970330T010000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
TZNAME:(DST)
TZOFFSETFROM:+000000
TZOFFSETTO:+010000
END:DAYLIGHT
X-TZINFO:Europe/Lisbon[2025b]
END:VTIMEZONE
BEGIN:VEVENT
UID:99e15556-fd88-4cb9-818e-fcbf853bc443
DTSTART:20250720T090000
DTEND:20250720T100000
CREATED:20250720T075329Z
DTSTAMP:20250720T075414Z
RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;UNTIL=20250817T080000Z
SUMMARY:Updated Title
TRANSP:OPAQUE
DESCRIPTION:New description
END:VEVENT
END:VCALENDAR`,
    },
  ];

  for (const test of testEvents) {
    const output = updateIcs(test.input.originalIcs, test.input.updates);
    assertEquals(output, test.expected);
  }
});

Deno.test('that parseVCalendar works', () => {
  const testIcs = `BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
BEGIN:VEVENT
UID:test-123
SUMMARY:Test Event
DTSTART:20250115T100000Z
DTEND:20250115T110000Z
ORGANIZER;CN=:mailto:test@example.com
TRANSP:OPAQUE
DESCRIPTION:Test description
LOCATION:Test location
URL:https://example.com
ATTENDEE;PARTSTAT=ACCEPTED;CN=Test Attendee:mailto:attendee@example.com
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:Test reminder
TRIGGER;VALUE=DATE-TIME:20250115T094500Z
END:VALARM
END:VEVENT
END:VCALENDAR`;

  const output = parseVCalendar(testIcs);

  assertEquals(output.length, 1);
  const event = output[0];
  assertEquals(event.uid, 'test-123');
  assertEquals(event.title, 'Test Event');
  assertEquals(event.description, 'Test description');
  assertEquals(event.location, 'Test location');
  assertEquals(event.eventUrl, 'https://example.com');
  assertEquals(event.transparency, 'opaque');
  assertEquals(event.attendees?.length, 1);
  assertEquals(event.reminders?.length, 1);
});

Deno.test('that parseVCalendar handles multiple events', () => {
  const testIcs = `BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:event-1
SUMMARY:Event 1
DTSTART:20250115T100000Z
DTEND:20250115T110000Z
END:VEVENT
BEGIN:VEVENT
UID:event-2
SUMMARY:Event 2
DTSTART:20250116T100000Z
DTEND:20250116T110000Z
END:VEVENT
END:VCALENDAR`;

  const output = parseVCalendar(testIcs);

  assertEquals(output.length, 2);
  assertEquals(output[0].uid, 'event-1');
  assertEquals(output[1].uid, 'event-2');
});

Deno.test('that parseVCalendar handles recurring events', () => {
  const testIcs = `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
BEGIN:VEVENT
UID:99e15556-fd88-4cb9-818e-fcbf853bc443
RECURRENCE-ID:20250721T080000Z
DTSTART:20250721T080000Z
DTEND:20250721T090000Z
CREATED:20250720T075329Z
DTSTAMP:20250720T075414Z
LAST-MODIFIED:20250720T075414Z
SUMMARY:Recurring Standup
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
UID:99e15556-fd88-4cb9-818e-fcbf853bc443
RECURRENCE-ID:20250722T080000Z
DTSTART:20250722T080000Z
DTEND:20250722T090000Z
CREATED:20250720T075329Z
DTSTAMP:20250720T075414Z
LAST-MODIFIED:20250720T075414Z
SUMMARY:Recurring Standup
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
UID:99e15556-fd88-4cb9-818e-fcbf853bc443
RECURRENCE-ID:20250723T080000Z
DTSTART:20250723T080000Z
DTEND:20250723T090000Z
CREATED:20250720T075329Z
DTSTAMP:20250720T075414Z
LAST-MODIFIED:20250720T075414Z
SUMMARY:Recurring Standup
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
UID:99e15556-fd88-4cb9-818e-fcbf853bc443
RECURRENCE-ID:20250724T080000Z
DTSTART:20250724T080000Z
DTEND:20250724T090000Z
CREATED:20250720T075329Z
DTSTAMP:20250720T075414Z
LAST-MODIFIED:20250720T075414Z
SUMMARY:Recurring Standup
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
UID:99e15556-fd88-4cb9-818e-fcbf853bc443
RECURRENCE-ID:20250725T080000Z
DTSTART:20250725T080000Z
DTEND:20250725T090000Z
CREATED:20250720T075329Z
DTSTAMP:20250720T075414Z
LAST-MODIFIED:20250720T075414Z
SUMMARY:Recurring Standup
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
UID:99e15556-fd88-4cb9-818e-fcbf853bc443
RECURRENCE-ID:20250728T080000Z
DTSTART:20250728T080000Z
DTEND:20250728T090000Z
CREATED:20250720T075329Z
DTSTAMP:20250720T075414Z
LAST-MODIFIED:20250720T075414Z
SUMMARY:Recurring Standup
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
UID:99e15556-fd88-4cb9-818e-fcbf853bc443
RECURRENCE-ID:20250729T080000Z
DTSTART:20250729T080000Z
DTEND:20250729T090000Z
CREATED:20250720T075329Z
DTSTAMP:20250720T075414Z
LAST-MODIFIED:20250720T075414Z
SUMMARY:Recurring Standup
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
UID:99e15556-fd88-4cb9-818e-fcbf853bc443
RECURRENCE-ID:20250730T080000Z
DTSTART:20250730T080000Z
DTEND:20250730T090000Z
CREATED:20250720T075329Z
DTSTAMP:20250720T075414Z
LAST-MODIFIED:20250720T075414Z
SUMMARY:Recurring Standup
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
UID:99e15556-fd88-4cb9-818e-fcbf853bc443
RECURRENCE-ID:20250731T080000Z
DTSTART:20250731T080000Z
DTEND:20250731T090000Z
CREATED:20250720T075329Z
DTSTAMP:20250720T075414Z
LAST-MODIFIED:20250720T075414Z
SUMMARY:Recurring Standup
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
UID:99e15556-fd88-4cb9-818e-fcbf853bc443
RECURRENCE-ID:20250801T080000Z
DTSTART:20250801T080000Z
DTEND:20250801T090000Z
CREATED:20250720T075329Z
DTSTAMP:20250720T075414Z
LAST-MODIFIED:20250720T075414Z
SUMMARY:Recurring Standup
TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
UID:99e15556-fd88-4cb9-818e-fcbf853bc443
RECURRENCE-ID:20250804T080000Z
DTSTART:20250804T080000Z
DTEND:20250804T090000Z
CREATED:20250720T075329Z
DTSTAMP:20250720T075414Z
LAST-MODIFIED:20250720T075414Z
SUMMARY:Recurring Standup
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR`;

  const output = parseVCalendar(testIcs);

  assertEquals(output.length, 11);
  assertEquals(output[0].uid, '99e15556-fd88-4cb9-818e-fcbf853bc443:20250721T080000Z');
  assertEquals(output[0].isRecurring, true);
  assertEquals(output[0].recurrenceMasterUid, '99e15556-fd88-4cb9-818e-fcbf853bc443');
  assertEquals(output[0].recurrenceId, '20250721T080000Z');
  assertEquals(output[0].title, 'Recurring Standup');

  assertEquals(output[1].uid, '99e15556-fd88-4cb9-818e-fcbf853bc443:20250722T080000Z');
  assertEquals(output[1].isRecurring, true);
  assertEquals(output[1].recurrenceMasterUid, '99e15556-fd88-4cb9-818e-fcbf853bc443');
  assertEquals(output[1].recurrenceId, '20250722T080000Z');
  assertEquals(output[1].title, 'Recurring Standup');
});

Deno.test('that getWeeksForMonth works', () => {
  const testDate = new Date('2025-01-15');
  const weeks = getWeeksForMonth(testDate);

  // January 2025 starts on Wednesday, so it should have 5 weeks
  assertEquals(weeks.length, 5);

  // First week should start with December 30, 2024 (Monday)
  assertEquals(weeks[0][0].date.getDate(), 30);
  assertEquals(weeks[0][0].date.getMonth(), 11);

  // Last week should end with February 2, 2025 (Sunday)
  assertEquals(weeks[4][6].date.getDate(), 2);
  assertEquals(weeks[4][6].date.getMonth(), 1);
});

Deno.test('that getDaysForWeek works', () => {
  const testDate = new Date('2025-01-15');
  const days = getDaysForWeek(testDate);

  assertEquals(days.length, 7);

  // Should start with Monday (January 13, 2025)
  assertEquals(days[0].date.getDate(), 13);
  assertEquals(days[0].date.getDay(), 1);

  // Should end with Sunday (January 19, 2025)
  assertEquals(days[6].date.getDate(), 19);
  assertEquals(days[6].date.getDay(), 0);

  // Each day should have 24 hours
  assertEquals(days[0].hours.length, 24);
});

Deno.test('that getCalendarEventStyle works', () => {
  const calendars: Calendar[] = [
    { url: 'cal-1', isVisible: true, uid: 'cal-1', name: 'Calendar 1', calendarColor: '#B51E1F' },
    { url: 'cal-2', isVisible: true, uid: 'cal-2', name: 'Calendar 2', calendarColor: '#1E3A89' },
  ];

  const opaqueEvent: CalendarEvent = {
    calendarId: 'cal-1',
    isAllDay: false,
    url: 'event-1.ics',
    uid: 'event-1',
    title: 'Opaque Event',
    startDate: new Date(),
    endDate: new Date(),
    organizerEmail: 'test@example.com',
    transparency: 'opaque',
    isRecurring: false,
    recurringRrule: undefined,
    sequence: 0,
  };

  const transparentEvent: CalendarEvent = {
    calendarId: 'cal-2',
    isAllDay: false,
    url: 'event-2.ics',
    uid: 'event-2',
    title: 'Transparent Event',
    startDate: new Date(),
    endDate: new Date(),
    organizerEmail: 'test@example.com',
    transparency: 'transparent',
    isRecurring: false,
    recurringRrule: undefined,
    sequence: 0,
  };

  assertEquals(getCalendarEventStyle(opaqueEvent, calendars), { backgroundColor: '#B51E1F' });
  assertEquals(getCalendarEventStyle(transparentEvent, calendars), { border: '1px solid #1E3A89' });
});

Deno.test('that getCalendarEventStyle returns default color for unknown calendar', () => {
  const calendars: Calendar[] = [];
  const event: CalendarEvent = {
    calendarId: 'unknown-cal',
    isAllDay: false,
    url: 'event-1.ics',
    uid: 'event-1',
    title: 'Unknown Calendar Event',
    startDate: new Date(),
    endDate: new Date(),
    organizerEmail: 'test@example.com',
    transparency: 'opaque',
    isRecurring: false,
    recurringRrule: undefined,
    sequence: 0,
  };

  assertEquals(getCalendarEventStyle(event, calendars), { backgroundColor: '#384354' });
});

Deno.test('that parseIcsDate works', () => {
  const tests: { input: string; expected: string }[] = [
    { input: '20250101T000000Z', expected: '2025-01-01T00:00:00.000Z' },
    { input: '20250201T000300', expected: '2025-02-01T00:03:00.000Z' },
    { input: '20250103T050000', expected: '2025-01-03T05:00:00.000Z' },
  ];

  for (const test of tests) {
    const output = parseIcsDate(test.input);
    assertEquals(output.toISOString(), test.expected);
  }
});

Deno.test('that convertRRuleToWords works', () => {
  const tests: { input: string; expected: string }[] = [
    { input: 'RRULE:FREQ=DAILY', expected: 'Every day' },
    { input: 'RRULE:FREQ=WEEKLY;BYDAY=MO', expected: 'Every week on Monday' },
    { input: 'RRULE:FREQ=MONTHLY;BYMONTHDAY=15', expected: 'Every month on the 15th' },
    { input: 'RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR', expected: 'Every week on Monday, Wednesday, Friday' },
    { input: 'RRULE:FREQ=MONTHLY;BYMONTHDAY=1;INTERVAL=2', expected: 'Every 2 months on the 1st' },
    { input: 'RRULE:FREQ=DAILY;COUNT=5', expected: 'Every day for 5 times' },
    {
      input: 'RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20250101T000000Z',
      expected: 'Every week on Monday, Wednesday, Friday until 2025-01-01',
    },
    {
      input: 'RRULE:FREQ=MONTHLY;BYMONTHDAY=1;INTERVAL=2;UNTIL=20250101T000000Z',
      expected: 'Every 2 months on the 1st until 2025-01-01',
    },
  ];

  for (const test of tests) {
    const output = convertRRuleToWords(test.input);
    assertEquals(output, test.expected);
  }
});

Deno.test('that convertRRuleToWords handles invalid rules', () => {
  const output = convertRRuleToWords('INVALID:RULE');
  assertEquals(output, '');
});

Deno.test('that parseVCalendar handles vCalendar 1.0', () => {
  const testIcs = `BEGIN:VCALENDAR
VERSION:1.0
BEGIN:VEVENT
UID:test-123
SUMMARY:Test Event
DTSTART:20250115T100000Z
DTEND:20250115T110000Z
END:VEVENT
END:VCALENDAR`;

  const output = parseVCalendar(testIcs);

  assertEquals(output.length, 1);
  assertEquals(output[0].uid, 'test-123');
});

Deno.test('that parseVCalendar handles multiline fields', () => {
  const testIcs = `BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:test-123
SUMMARY:Test Event with
 long title
DESCRIPTION:Test description with very
 long text.\\n\\nAnd a new line.
DTSTART:20250115T100000Z
DTEND:20250115T110000Z
END:VEVENT
END:VCALENDAR`;

  const output = parseVCalendar(testIcs);

  assertEquals(output.length, 1);
  assertEquals(output[0].title, 'Test Event with long title');
  assertEquals(output[0].description, 'Test description with very long text.\n\nAnd a new line.');
});

Deno.test('that parseVCalendar handles empty fields gracefully', () => {
  const testIcs = `BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:test-123
SUMMARY:
DESCRIPTION:
LOCATION:
END:VEVENT
END:VCALENDAR`;

  const output = parseVCalendar(testIcs);

  assertEquals(output.length, 1);
  assertEquals(output[0].uid, 'test-123');
  assertEquals(output[0].title, undefined);
  assertEquals(output[0].location, undefined);
  assertEquals(output[0].description, undefined);
});
